/*
 * @(#) $Header$
 *
 * Copyright (C)  2006  Daniel Léonard
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */

package ca.forklabs.baselib.util.iterator;

import java.util.ListIterator;
import ca.forklabs.baselib.util.iterator.IterableIterator;
import ca.forklabs.baselib.util.iterator.Resources;

import java.util.NoSuchElementException;

/**
 * Class {@code ArrayIterator} is a <em>bidirectional</em> <em>alterable</em>
 * but <em>non-appendable</em> iterator for arrays.
 *
 * @param   <E>   the type of the array's elements.
 *
 * @author <a href="mailto:forklabs at gmail.com?subject=ca.forklabs.baselib.util.iterator.ArrayIterator">Daniel Léonard</a>
 * @version $Revision$
 */
public class ArrayIterator<E> extends IterableIterator<E> implements ListIterator<E> {

//---------------------------
// Class variables
//---------------------------

   /** Value indicating the index is invalid.*/
   protected static final int INVALID_INDEX = -1;


//---------------------------
// Instance variables
//---------------------------

   /** The current cursor position. */
   private int the_cursor;

   /** The underlying array. */
   private E[] the_array;

   /** The position of the last returned object. */
   private int last_cursor;


//---------------------------
// Constructor
//---------------------------

   /**
    * Creates an iterator over the specified array starting at index <em>0</em>.
    * @param   array   the array to iterate over.
    * @exception   NullPointerException   if {@code array} is {@code null}.
    */
   @SafeVarargs
   public ArrayIterator(E... array) {
      this(array, 0);
      }

   /**
    * Creates an iterator over the specified array starting at the specified
    * position.
    * @param   array   the array to iterate over.
    * @param   initial_position   the index of the element to be first returned
    *                             by {@link #next()}.
    * @exception   NullPointerException   if {@code array} is {@code null}.
    * @exception   IndexOutOfBoundsException   if {@code initial_position} is
    *                                          not within the array bounds
    *                                          {@code [0, array.length]}.
    */
   public ArrayIterator(E[] array, int initial_position) {
      this.set(array, initial_position);
      }


//---------------------------
// Accessors and mutators
//---------------------------

   /**
    * Changes the internal state of the iterator.
    * @param   array   the array to iterate over.
    * @param   initial_position   the index of the element to be first returned
    *                             by {@link #next()}.
    * @exception   NullPointerException   if {@code array} is {@code null}.
    * @exception   IndexOutOfBoundsException   if {@code initial_position} is
    *                                          not within the array bounds
    *                                          {@code [0, array.length]}.
    */
   protected void set(E[] array, int initial_position) {
      if (null == array) {
         String message = this.getCannotIterateErrorMessage();
         throw new NullPointerException(message);
         }

      boolean within = ArrayIterator.withinBounds(array, initial_position);
      if (within == false) {
         String message = this.getNotWithinBoundsErrorMessage(initial_position, array.length);
         throw new IndexOutOfBoundsException(message);
         }

      this.the_array = array;
      this.setPosition(initial_position);
      this.setLast(ArrayIterator.INVALID_INDEX);
      }

   /**
    * Gets the position of the cursor, the index of next element to return
    * by a call to {@link #next()}.
    * @return   the position of the cursor.
    */
   protected int getPosition() {
      return this.the_cursor;
      }

   /**
    * Changes the position of the cursor without checks.
    * @param   cursor   the new position of the cursor.
    */
   protected void setPosition(int cursor) {
      this.the_cursor = cursor;
      }

   /**
    * Gets the underlying array.
    * @return   the underlying array.
    */
   protected E[] getArray() {
      return this.the_array;
      }

   /**
    * Gets the index of the last returned element, either by {@link #next()} or
    * by {@link #previous()}.
    * @return   the index of the last returned element.
    */
   protected int getLast() {
      return this.last_cursor;
      }

   /**
    * Changes the index of the last returned element without checks.
    * @param   pos   the index of the last returned element.
    */
   protected void setLast(int pos) {
      this.last_cursor = pos;
      }


//---------------------------
// Implemented methods from java.util.ListIterator
//---------------------------

   /**
    * Unsupported operation.
    * @param   element   ignored.
    * @exception   UnsupportedOperationException   always.
    */
   @Override
   @SuppressWarnings("nls")
   public void add(E element) throws UnsupportedOperationException {
      String message = this.getUnsupportedMethodErrorMessage("remove()");
      throw new UnsupportedOperationException(message);
      }

   /**
    * Returns {@code true} if this list iterator has more elements when
    * traversing the list in the forward direction. The list has more elements
    * if the cursor is not passed the last element of the underlying array.
    * @return   {@code true} if the list iterator has more elements when
    *           traversing the list in the forward direction, {@code false}
    *           otherwise.
    */
   @Override
   public boolean hasNext() {
      E[] array = this.getArray();
      int cursor = this.getPosition();
   // there are no next elements if the cursor is outside the arrays bound
      boolean within = ArrayIterator.withinBoundsStrict(array, cursor);
      return within;
      }

   /**
    * Returns {@code true} if this list iterator has more elements when
    * traversing the list in the reverse direction. The list has more elements
    * if the cursor is not passed the first element of the underlying array.
    * @return   {@code true} if the list iterator has more elements when
    *           traversing the list in the reverse direction, {@code false}
    *           otherwise.
    */
   @Override
   public boolean hasPrevious() {
      E[] array = this.getArray();
      int cursor = this.getPosition();
   // the cursor must be within the bounds ...
      boolean within = ArrayIterator.withinBounds(array, cursor);
   // ... but not on the first element
      within = within && (0 < cursor);
      return within;
      }

   /**
    * Returns the next element in the list. This method may be called repeatedly
    * to iterate through the list, or intermixed with calls to
    * {@link #previous()} to go back and forth - note that
    * alternating calls to {@link #next()} and
    * {@link #previous()} will return the same element repeatedly.
    * @return   the next element in the list.
    * @exception   NoSuchElementException   if the iteration has no next
    *                                       element.
    */
   @Override
   public E next() {
      boolean has_next = this.hasNext();
      if (has_next == false) {
         String message = this.getNoMoreForNextErrorMessage();
         throw new NoSuchElementException(message);
         }

      E[] array = this.getArray();
      int index = this.nextIndex();
      E element = array[index];

      this.setLast(index);
      this.setPosition(index + 1);

      return element;
      }

   /**
    * Returns the index of the element that would be returned by a subsequent
    * call to {@link #next()}. It returns the list size if the list
    * iterator is at the end of the list.
    * @return   the index of the element that would be returned by a subsequent
    *           call to {@link #next()}, or the array size if list
    *           iterator is at end of list.
    */
   @Override
   public int nextIndex() {
      int index = this.getPosition();
      return index;
      }

   /**
    * Returns the previous element in the list. This method may be called
    * repeatedly to iterate through the list backwards, or intermixed with calls
    * to {@link #next()} to go back and forth - note that
    * alternating calls to {@link #next()} and
    * {@link #previous()} will return the same element repeatedly.
    * @return   the previous element in the list.
    * @exception   NoSuchElementException   if the iteration has no previous
    *                                       element.
    */
   @Override
   public E previous() {
      boolean has_previous = this.hasPrevious();
      if (has_previous == false) {
         String message = this.getNoMoreForPreviousErrorMessage();
         throw new NoSuchElementException(message);
         }

      E[] array = this.getArray();
      int index = this.previousIndex();
      E element = array[index];

      this.setLast(index + 1);
      this.setPosition(index);

      return element;
      }

   /**
    * Returns the index of the element that would be returned by a subsequent
    * call to {@link #previous()}. It returns <em>-1</em> if the
    * list iterator is at the beginning of the list.
    * @return   The index of the element that would be returned by a subsequent
    *           call to {@link #previous()}, or <em>-1</em> if list
    *           iterator is at beginning of list.
    */
   @Override
   public int previousIndex() {
      int index = this.getPosition() - 1;
      return index;
      }

   /**
    * Unsupported operation.
    * @exception   UnsupportedOperationException   always.
    */
   @Override
   @SuppressWarnings("nls")
   public void remove() throws UnsupportedOperationException {
      String message = this.getUnsupportedMethodErrorMessage("remove()");
      throw new UnsupportedOperationException(message);
      }

   /**
    * Replaces the last element returned by {@link #next()} or
    * {@link #previous()} with the specified element.
    * @param   element   The element by which to replace the last element
    *                    returned by {@link #next()} or
    *                    {@link #previous()}.
    * @exception   IllegalStateException   if neither
    *                                      {@link #next()} nor
    *                                      {@link #previous()} has
    *                                      already been called.
    * @exception   ClassCastException   if the element is not of a correct type
    *                                   for the underlying array
    */
   @Override
   public void set(E element) {
      int last = this.getLast();
      if (ArrayIterator.INVALID_INDEX == last) {
         String message = this.getNoCallToNextOrPreviousErrorMessage();
         throw new IllegalStateException(message);
         }

      E[] array = this.getArray();
      array[last] = element;
      }


//---------------------------
// Instance methods
//---------------------------

   /**
    * Gets the error message telling that it is impossible to iterate.
    * @return   the formatted message.
    */
   protected String getCannotIterateErrorMessage() {
      String key = Resources.ARRAY_ITERATOR_CANNOT_ITERATE;
      String message = Resources.getLocalizedString(key);
      return message;
      }

   /**
    * Gets the error message telling that the cursor is not within bounds.
    * @param   position   the position of the cursor.
    * @param   max   the limit of the interval.
    * @return   the formatted message.
    */
   @SuppressWarnings("boxing")
   protected String getNotWithinBoundsErrorMessage(int position, int max) {
      String key = Resources.ARRAY_ITERATOR_NOT_WITHIN_BOUNDS;
      String message = Resources.getLocalizedString(key, position, max);
      return message;
      }

   /**
    * Gets the error message telling that a method is not supported.
    * @param   method   the name of the unsupported method.
    * @return   the formatted message.
    */
   protected String getUnsupportedMethodErrorMessage(String method) {
      String key = Resources.ITERATOR_UNSUPPORTED_METHOD;
      String message = Resources.getLocalizedString(key, method);
      return message;
      }

   /**
    * Gets the error message telling that there are not more elements for {@link #next()}.
    * @return   the formatted message.
    */
   protected String getNoMoreForNextErrorMessage() {
      String key = Resources.ARRAY_ITERATOR_NO_MORE_NEXT;
      String message = Resources.getLocalizedString(key);
      return message;
      }

   /**
    * Gets the error message telling that there are not more elements for {@link #previous()}.
    * @return   the formatted message.
    */
   protected String getNoMoreForPreviousErrorMessage() {
      String key = Resources.ARRAY_ITERATOR_NO_MORE_PREVIOUS;
      String message = Resources.getLocalizedString(key);
      return message;
      }

   /**
    * Gets the error message telling that there needs to be a call to either
    * {@link #next()} or {@link #previous()}.
    * @return   the formatted message.
    */
   protected String getNoCallToNextOrPreviousErrorMessage() {
      String key = Resources.ARRAY_ITERATOR_NO_CALL_TO_NEXT_OR_PREVIOUS;
      String message = Resources.getLocalizedString(key);
      return message;
      }


//---------------------------
// Class methods
//---------------------------

   /**
    * Determines if the cursor value is valid. The cursor value is valid if it
    * is within <code>[0, array.length]</code>.
    * @param   array   the array.
    * @param   cursor   the cursor position.
    * @return   {@code true} if the cursor value is valid, {@code false}
    *           otherwise.
    */
   protected static boolean withinBounds(Object[] array, int cursor) {
      boolean within = ((0 <= cursor) && (array.length >= cursor));
      return within;
      }

   /**
    * Determines if the cursor value is valid. The cursor value is valid if it
    * is within <code>[0, array.length[</code>.
    * @param   array   the array.
    * @param   cursor   the cursor position.
    * @return   {@code true} if the cursor value is valid, {@code false}
    *           otherwise.
    */
   protected static boolean withinBoundsStrict(Object[] array, int cursor) {
      boolean within = ((0 <= cursor) && (array.length > cursor));
      return within;
      }

   }
